package lumis.portal.structure;

import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.UnexpectedException;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.propertybag.Property;
import lumis.portal.propertybag.PropertyBag;
import lumis.util.XmlUtil;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * The base class for portal structure elements.
 *
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 * @since 5.6.0
 */
public abstract class StructureElement
{
	/**
	 * A map of local properties. Lazily created at {@link #getPropertyBagLocalValues()}.
	 * @since 5.6.0
	 */
	private transient Map<String, List<String>> propertyBagLocalValues;
	/**
	 * A map of inherited properties. Lazily created at {@link #getPropertyBagInheritedValues()}. 
	 * @since 5.6.0
	 */
	private transient Map<String, List<String>> propertyBagInheritedValues;
	/**
	 * A map of edited properties. Lazily created at {@link #getPropertyBagEditedValues()}.
	 * @since 5.6.0
	 */
	private transient Map<String, List<String>> propertyBagEditedValues;
	/**
	 * A set of properties to be deleted. Lazily created at {@link #getPropertyBagDeletedProperties()}.
	 * @since 5.6.0
	 */
	private transient Set<String> propertyBagDeletedProperties;

	/**
	 * Returns the local property values.
	 * @return the local property values.
	 * @since 5.6.0
	 */
	private Map<String, List<String>> getPropertyBagLocalValues()
	{
		if (propertyBagLocalValues == null)
			propertyBagLocalValues = new HashMap<String, List<String>>();
		return propertyBagLocalValues;
	}

	/**
	 * Returns the inherited property values.
	 * @return the inherited property values.
	 * @since 5.6.0
	 */
	private Map<String, List<String>> getPropertyBagInheritedValues()
	{
		if (propertyBagInheritedValues == null)
			propertyBagInheritedValues = new HashMap<String, List<String>>();
		return propertyBagInheritedValues;
	}

	/**
	 * Returns the edited property values.
	 * @return the edited property values.
	 * @since 5.6.0
	 */
	private Map<String, List<String>> getPropertyBagEditedValues()
	{
		if (propertyBagEditedValues == null)
			propertyBagEditedValues = new HashMap<String, List<String>>();
		return propertyBagEditedValues;
	}

	/**
	 * Returns the deleted property names.
	 * @return the deleted property names.
	 * @since 5.6.0
	 */
	private Set<String> getPropertyBagDeletedProperties()
	{
		if (propertyBagDeletedProperties == null)
			propertyBagDeletedProperties = new HashSet<String>();
		return propertyBagDeletedProperties;
	}

	/**
	 * Returns the property bag identifier.
	 * @return The propertyBagId.
	 * @since 5.6.0
	 */
	protected abstract String getPropertyBagId();
	
	/**
	 * Returns the property name spaces. Used to decides if a property can be updated.
	 * 
	 * @return property name spaces
	 * @since 5.6.0
	 */
	protected abstract Pattern getManagedPropertiesPattern();
	
	/**
	 * Returns whether a property can be edited in this element.
	 * <p>
	 * This implementation is based on the {@link #getManagedPropertiesPattern()}.
	 * 
	 * @param propertyName the property name.
	 * @return <code>true</code> if a property can be edited in this element, else returns <code>false</code>.
	 * @since 5.6.0
	 */
	protected boolean isManagedProperty(String propertyName)
	{
		return getManagedPropertiesPattern().matcher(propertyName).matches();
	}
	
	/**
	 * Returns the property value for a given property name.
	 * The property value could be local or inherited.
	 * 
	 * @param propertyName the property name.
	 * @return the property value.
	 * 
	 * 
	 * @since 5.6.0
	 */
	protected String getPropertyValue(String propertyName)
	{
		checkManagedProperty(propertyName);
		
		String result = getLocalPropertyValue(propertyName);
		if (result == null)
		{
			result = getInheritedPropertyValue(propertyName);
		}
		return result;
	}
	
	/**
	 * Gets the property value for a given property name from inherited properties.
	 * 
	 * @param propertyName the property name.
	 * @return the property value.
	 * @since 5.6.0
	 */
	private String getInheritedPropertyValue(String propertyName)
	{
		String result = null;
		List<String> inheritedStructureValues = getPropertyBagInheritedValues().get(propertyName);
		if(inheritedStructureValues != null && !inheritedStructureValues.isEmpty())
		{
			result = inheritedStructureValues.get(0);
		}
		return result;
	}

	/**
	 * Gets the property for a given property name from local properties.
	 * 
	 * @param propertyName the property name.
	 * @return the property value.
	 * @since 5.6.0
	 */
	private String getLocalPropertyValue(String propertyName)
	{
		String result = null; 
		if(!getPropertyBagDeletedProperties().contains(propertyName))
		{
			if(getPropertyBagEditedValues().containsKey(propertyName))
			{
				List<String> editedValues = getPropertyBagEditedValues().get(propertyName);
				if(editedValues != null && !editedValues.isEmpty())
					result = editedValues.get(0);
			}
			else
			{
				List<String> localValues = getPropertyBagLocalValues().get(propertyName);
				if(localValues != null && !localValues.isEmpty())
				{
					result = localValues.get(0);
				}
			}
		}
		return result;
	}
	
	/**
	 * Sets on the local property structure map a given <code>propertyValue</code> 
	 * property value for a given <code>propertyName</code> property name.
	 * 
	 * @param propertyName the property name.
	 * @param propertyValue the property value.
	 * @since 5.6.0
	 */
	protected void setPropertyValue(String propertyName, String propertyValue)
	{
		checkManagedProperty(propertyName);
		
		getPropertyBagEditedValues().put(propertyName, Arrays.asList(propertyValue));
		getPropertyBagDeletedProperties().remove(propertyName);
	}
	
	/**
	 * Removes a property for a given property name from the local property structure map.
	 * 
	 * @param propertyName the name of the property to be removed.
	 * @since 5.6.0
	 */
	protected void removeProperty(String propertyName)
	{
		checkManagedProperty(propertyName);
		
		getPropertyBagEditedValues().remove(propertyName);
		getPropertyBagDeletedProperties().add(propertyName);
	}

	/**
	 * Checks if the property name is managed for this.
	 * 
	 * @param propertyName the property name to be checked.
	 * @since 5.6.0
	 */
	private void checkManagedProperty(String propertyName)
	{
		if(!isManagedProperty(propertyName))
			throw new IllegalArgumentException("property '" + propertyName + "' is not managed by " + this);
	}
	
	/**
	 * Loads the values of the property bag of this element.
	 * <p>
	 * Only the properties that can be changed are actually loaded.
	 * 
	 * @see #isManagedProperty(String)
	 * 
	 * @since 5.6.0
	 */
	protected void loadPropertyBagValues() throws PortalException
	{
		// clear all property maps
		getPropertyBagInheritedValues().clear();
		getPropertyBagLocalValues().clear();
		
		PropertyBag propertyBag = ManagerFactory.getPropertyBagManager().get(getPropertyBagId());
		PropertyBag parentPropertyBag = propertyBag.getParentPropertyBag();
		if(parentPropertyBag != null)
		{
			getPropertyBagInheritedValues().putAll(getManagedPropertiesMap(parentPropertyBag.getPropertyMap()));
		}
		getPropertyBagLocalValues().putAll(getManagedPropertiesMap(propertyBag.getLocalPropertyMap()));
	}
	
	/**
	 * Returns a map with the managed properties values from the given property map.
	 * 
	 * @param propertyMap 
	 * @return 
	 * @since 5.6.0
	 */
	private Map<String, List<String>> getManagedPropertiesMap(Map<String, Property> propertyMap)
	{
		Map<String, List<String>> structurePropertyValuesMap = new HashMap<String, List<String>>();
		for(String propertyName:propertyMap.keySet())
		{
			// verify if the property is a structure property
			if(isManagedProperty(propertyName))
			{
				structurePropertyValuesMap.put(propertyName, new ArrayList<String>(propertyMap.get(propertyName).getDefaultValues()));
			}
		}
		return structurePropertyValuesMap;
	}
	
	/**
	 * Apply the changes made to the properties managed by this element to the property
	 * bag persistence entities.
	 *
	 * @see #isManagedProperty(String)
	 * 
	 * @since 5.6.0
	 */
	protected void applyPropertyBagChanges() throws PortalException
	{
		// get PropertyBag
		PropertyBag propertyBag = ManagerFactory.getPropertyBagManager().get(getPropertyBagId());
		
		// update edited properties
		for(String propertyName:getPropertyBagEditedValues().keySet())
		{
			propertyBag.setDefaultValue(propertyName, getPropertyBagEditedValues().get(propertyName).get(0));
			propertyBag.getProperty(propertyName).setPrivate(true);
		}
		getPropertyBagEditedValues().clear();
		
		// remove deleted properties
		for(String propertyName:getPropertyBagDeletedProperties())
		{
			try
			{
				propertyBag.removeProperty(propertyName);
			}
			catch (PortalObjectNotFoundException e) 
			{
				// ignore
			}
		}
		getPropertyBagDeletedProperties().clear();

		loadPropertyBagValues();
	}
	
	/**
	 * Serialize the property bags.
	 * <p>
	 * Returns the serialized property bags in the following format:<br />
	 * 
	 * <xmp>
	 * <propertyBag>
	 *   <p>
	 *     <n>propertyName</n>
	 *     <pvt>isPrivate</pvt>
	 *     <vs>
	 *       <v>propertyValue 1</v>
	 *       <v>propertyValue 2</v>
	 *       ...
	 *     </vs>
	 *   </p>
	 *   <p>
	 *   ...
	 *   </p>
	 *   ...
	 * </propertyBag>
	 * </xmp>
	 * 
	 * @return Serialized property bags. 
	 * @throws PortalException
	 * @since 5.6.0
	 */
	protected String serializePropertyBag() throws PortalException
	{
		StringBuilder stream = new StringBuilder(100);
		
		// stream property bags (note that the property bags are not read in deserialize(Node),
		// instead they are read in the Manager)
		PropertyBag propertyBag = ManagerFactory.getPropertyBagManager().get(getPropertyBagId());
		Map<String, Property> propertyBagProperties = propertyBag.getLocalPropertyMap();
		if(propertyBagProperties.size() > 0)
		{
			stream.append("<propertyBag>");
			
			for(Map.Entry<String, Property> entry: propertyBagProperties.entrySet())
			{
				Property property = entry.getValue();
				List<String> defaultValues = property.getDefaultValues();
				
				// a null defaultValues means the property is inheriting
				// its values. For simplicity, this property will be
				// ignored (note that this may not be appropriate if there
				// is any other important information on the property)
				if (defaultValues != null)
				{
					// append property
					stream.append("<p>");
					stream.append("<n>"+XmlUtil.encodeXml(property.getName())+"</n>");
					stream.append("<pvt>"+property.isPrivate()+"</pvt>");
					
					// append property's defaultValues
					stream.append("<vs>");
					for(String value : defaultValues)
					{
						stream.append("<v>"+XmlUtil.encodeXml(value)+"</v>");
					}
					stream.append("</vs>");
					
					stream.append("</p>");
				}
			}
			stream.append("</propertyBag>");
		}
		
		return stream.toString();
	}

	/**
	 * Generates a friendly identifier for the element based on the given value.
	 * 
	 * @param value
	 *            the value to use to generate friendly identifier.
	 * @return the generated friendly identifier.
	 * @since 6.0.0
	 */
	protected String getAutomaticFriendlyId(String value) throws PortalException
	{
		if (value == null)
			throw new IllegalArgumentException("Value cannot be null.");

		String baseValue = getFriendlyIdBaseValue(value);
		String currentValue;
		Integer counter = null;
		while (!(mayUseFriendlyId(currentValue = getFriendlyIdValueForCounter(baseValue, counter))))
		{
			if (counter == null)
				counter = new Integer(1);
			else
				counter = new Integer(counter.intValue() + 1);
		}
		return currentValue;
	}

	/**
	 * Indicates if the element may use the given friendly identifier.
	 * 
	 * @param friendlyId
	 *            the friendly identifier.
	 * @return true if the element may use the given friendly identifier, false
	 *         otherwise.
	 * @since 6.0.0
	 */
	protected abstract boolean mayUseFriendlyId(String friendlyId) throws PortalException;

	/**
	 * Returns the base value from the given value. The base value should
	 * replace the forbidden characters, truncate the value to the maximum
	 * length allowed and do any other necessary processing.
	 * 
	 * @param value
	 *            the original value.
	 * @return the processed base value.
	 * @since 6.0.0
	 */
	protected String getFriendlyIdBaseValue(String value) throws PortalException
	{
		value = stripAccents(value).trim().replaceAll("[^\\w]", "-").replaceAll("--+", "-")
				.replaceAll("^-|-$", "").toLowerCase(Locale.ENGLISH);
		int friendlyIdMaxLenght = getFriendlyIdMaxLength();

		if (value.length() > friendlyIdMaxLenght)
			value = value.substring(0, friendlyIdMaxLenght);

		if (value.length() == 0)
			value = "-";

		return value;
	}

	/**
	 * Removes accents from characters.
	 * 
	 * @param value
	 *            the original value.
	 * @return the processed value.
	 * @since 6.0.0
	 */
	protected String stripAccents(String value) throws PortalException
	{
		try
		{
			Class<?> accentFilterClass = ManagerFactory.getDeploymentManager().getClassLoader().loadClass("lumis.search.lucene.AccentFilter");
			Method stripAccentChars = accentFilterClass.getDeclaredMethod("stripAccentChars", new Class[]{String.class});
			String ret = (String) stripAccentChars.invoke(null, new Object[] { value });
			return ret;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	/**
	 * Returns the result of base value inserting the given counter.
	 * <p>
	 * If the counter is <code>null</code> then no counter should be used.
	 * 
	 * @param baseValue
	 *            the base value.
	 * @param counter
	 *            the counter.
	 * @return the base value with the given counter inserted.
	 * @since 6.0.0
	 */
	protected String getFriendlyIdValueForCounter(String baseValue, Integer counter)
			throws PortalException
	{
		int maxLength = getFriendlyIdMaxLength();
		if (counter == null)
		{
			if (baseValue.length() > maxLength)
				return baseValue.substring(0, maxLength);
			return baseValue;
		}
		String counterString = "-" + Integer.toString(counter.intValue());
		int counterLength = counterString.length();

		// TODO we will not treat the case that counter length > max size right
		// now

		int availableLength = maxLength - counterLength;
		if (baseValue.length() > availableLength)
			baseValue = baseValue.substring(0, availableLength);

		return baseValue + counterString;
	}

	/**
	 * Returns the maximum length of friendly identifier.
	 * 
	 * @return the maximum length of friendly identifier.
	 * @since 6.0.0
	 */
	protected int getFriendlyIdMaxLength()
	{
		return 50;
	}
}